Big Data házi feladat¶

Készítők: Csilling Tamás és Knyihár Gábor

Feladatkiírás¶

Azonosító: BKK3

Téma¶

Az I épületből este 10-kor, tömegközlekedéssel elérhető "kocsmák" (i.e., Google Maps találat erre a keresésre) elemzése

Adatbeszerzés¶

https://openmobilitydata.org/p/bkk/42 és pl. Google Places API

Adatelőkészítés és szűkítés¶

A Google Places API (a free változat elég kell, hogy legyen) talán a fő kihívás, egyenértékes megoldást elfogadok

Leskálázás méretben "small" datára¶

Valószínűleg nem szükséges

EDA fókusz¶

Az I épület 10 perces sétatávolságában megállóval rendelkező éjszakai járatok alapvető jellemzői (javasolt: pandas_profiling vagy BambooLib)

Big Data vizualizáció¶

Datashader alapú (lehet HoloViz-be integrálva) interaktív, Budapest-térkép alapú heatmap, mely az I épületből este 10-es indulással, átszállás nélkül való utazási időben vett távolságát mutatja a "celláknak" (cellán belül átlagolással, ha több opció is van). Természetesen vannak a városnak olyan részei, ahova átszállás nélkül nem lehet eljutni az egyetemről!

Elemzési feladat¶

Algoritmus azon megállók megtalálására, ahova 45 perc alatt, max k átszállással el lehet jutni az I épületből este 10-es indulással. (Demonstráció elég k=1 vagy 2-re.)

BKK adatok beszerzése, előkészítése és szűkítése¶

Az adatok elérhetőek a https://www.bkk.hu/gtfs/budapest_gtfs.zip linken, melyek GTFS formátumban vannak. Az elérhető adatok struktúráját mutatja a következő ábra:

Chart

Ezek számos felesleges adatot is tartalmaznak, így ezt átalakítottuk és összefésültük egy nagy adathalmazba. Az oszlopok megfeleltetéseit mutatja a következő táblázat.

Változó név Eredeti tábla Eredeti név
agency_name agency agency_name
route_name routes route_short_name
route_type routes route_type
route_desc routes route_desc
trip_id trips trip_id
trip_direction trips direction_id
trip_wheelchair_accessible trips wheelchair_accessible
trip_bikes_allowed trips bikes_allowed
trip_boarding_door trips boarding_door
stop_arrival_time stop_times arrival_time
stop_departure_time stop_times departure_time
stop_id stops stop_id
stop_name stops stop_name
stop_latitude stops stop_lat
stop_longitude stops stop_lon
stop_location_type stops location_type
stop_wheelchair_boarding stops wheelchair_boarding

Ezen felül néhány érték esetében alapértelmezetten NaN értékek kerültek be a táblázatba, ezeket át kellett alakítani, valamint kezelni kellett a időformátumokat is. A GTFS-ben az éjfél után közlekedő járatok hivatalosan még az adott naphoz tartoznak, ezért például egy 01:00-kor közlekedő járat ideje 25:00-ként van megjelölve. Ez viszont értelmezhetetlen a programkódban, így ezeket is át kellett alakítani. Viszont jelölni kellett, hogy az a következő napon van, azért, hogy könnyen lehessen kezelni az összehasonlításokat. A végeredményt mentettük parquett fájlba.

In [52]:
## ----- Imports -----

from datetime import datetime, timedelta
import os
import requests
import zipfile
import pandas as pd
import io

## ----- Helpers  -----

def downloadFile(url, path, chunk_size=128):
    """Download zip file from url to path"""
    if os.path.isfile(path):
        return
    r = requests.get(url, stream=True)
    with open(path, 'wb') as fd:
        for chunk in r.iter_content(chunk_size=chunk_size):
            fd.write(chunk)

def getDataFrameFromZip(path, filename):
    """Extract pandas dataframe from zip file"""
    archive = zipfile.ZipFile(path, 'r')
    csv = archive.read(filename).decode("utf-8")
    handler = io.StringIO(csv)
    return pd.read_csv(handler,low_memory=False)

def convertTime(time):
    """Convert time string to time objects"""
    hours = int(time[0:2])
    if hours < 24:
        return datetime.strptime(time, "%H:%M:%S")
    else:
        new_time = str(hours-24).zfill(2) + time[2:]
        return datetime.strptime(new_time, "%H:%M:%S") + timedelta(days=1)

## ----- Variables -----

parquet_path = "temp/bkk.parquet.gzip"

## ----- Script -----

def import_bkk():
    url = "https://www.bkk.hu/gtfs/budapest_gtfs.zip"
    temp_folder = "temp"
    zip_path = "temp/bkk.zip"

    if not os.path.isdir(temp_folder):
        os.mkdir(temp_folder)

    if not os.path.exists(parquet_path):
        # Download zip file
        downloadFile(url, zip_path)

        # Collect and clean data
        agency = getDataFrameFromZip(zip_path, "agency.txt")[["agency_id", "agency_name"]].set_index("agency_id")

        routes = getDataFrameFromZip(zip_path,"routes.txt")[["agency_id", "route_id", "route_short_name", "route_type", "route_desc"]].set_index(["route_id", "agency_id"])
        routes["route_type"] = routes["route_type"].astype(int)
        routes["route_type"] = routes["route_type"].replace([0,1,3,4,11,109],[0,1,2,3,4,5])
        routes = routes.rename(columns={'route_short_name': 'route_name'})

        trips = getDataFrameFromZip(zip_path,"trips.txt")[["route_id", "trip_id", "direction_id","wheelchair_accessible", "bikes_allowed", "boarding_door"]].set_index(["trip_id", "route_id"])
        trips = trips.fillna(0)
        trips["direction_id"] = trips["direction_id"].astype(int)
        trips["wheelchair_accessible"] = trips["wheelchair_accessible"].astype(int)
        trips["bikes_allowed"] = trips["bikes_allowed"].astype(int)
        trips["boarding_door"] = trips["boarding_door"].astype(int)
        trips["boarding_door"] = trips["boarding_door"].replace([0,2],[0,1])
        trips = trips.rename(columns={'direction_id': 'trip_direction', 'wheelchair_accessible': 'trip_wheelchair_accessible', 'bikes_allowed':'trip_bikes_allowed', 'boarding_door': 'trip_boarding_door'})

        stop_times = getDataFrameFromZip(zip_path,"stop_times.txt")[["stop_id", "trip_id", "arrival_time", "departure_time"]].set_index(["stop_id", "trip_id"])
        stop_times["departure_time"] = stop_times.departure_time.apply(convertTime)
        stop_times["arrival_time"] = stop_times.arrival_time.apply(convertTime)
        stop_times = stop_times.rename(columns={'arrival_time': 'stop_arrival_time', 'departure_time':'stop_departure_time'})

        stops = getDataFrameFromZip(zip_path,"stops.txt")[["stop_id", "stop_name", "stop_lat", "stop_lon", "location_type", "wheelchair_boarding"]].set_index("stop_id")
        stops = stops.fillna(0)
        stops["location_type"] = stops["location_type"].astype(int)
        stops["wheelchair_boarding"] = stops["wheelchair_boarding"].astype(int)
        stops = stops.rename(columns={'stop_lat': 'stop_latitude', 'stop_lon':'stop_longitude', 'location_type':'stop_location_type', 'wheelchair_boarding': 'stop_wheelchair_boarding'})

        # Join data and drop unnecessary columns
        all_data = trips.join(routes).join(stop_times).join(stops).join(agency)
        all_data = all_data.reset_index().drop(columns=["route_id", "agency_id"])

        # Order columns
        all_data = all_data[["agency_name","route_name","route_type","route_desc", "trip_id", "trip_direction",  "trip_wheelchair_accessible", "trip_bikes_allowed",  "trip_boarding_door",  "stop_arrival_time", "stop_departure_time",  "stop_id", "stop_name", "stop_latitude", "stop_longitude", "stop_location_type", "stop_wheelchair_boarding"]]

        # Export to parquet
        all_data.to_parquet(parquet_path, compression='gzip')

    else:
        # Import from parquet
        all_data = pd.read_parquet(parquet_path)

    return all_data

df = import_bkk()

BKK adatok értelmezése¶

Az adatokat parquet fájlból töltjük be, mely a következő oszlopokat tartalmazza:

Változó Változó neve Típus Megengedett értékek Leírás
Szolgáltató neve agency_name Szöveg A szolgáltató teljes neve.
Járat neve route_name Szöveg Az járat rövid neve.
Járat típusa route_type Egész szám 0 - villamos, 1 - metró, 2 - busz, 3 - hajó, 4 - troli, 5 - hév A járatot kiszolgáló jármű típusa.
Járat leírása route_desc Szöveg A járat rövid leírása.
Útvonal azonosítója trip_id Szöveg Két megálló közötti utazás azonosítója.
Útvonal iránya trip_direction Egész szám 0 - normál, 1 - ellentétes Az utazás menetirányát jelzi.
Kerekesszékkel elérhető trip_wheelchair_accessible Egész szám 0 - ismeretlen, 1 - igen, 2 - nem Azt jelzi, hogy a járaton kerekesszékkel lehetséges-e utazni.
Kerékpárok engedélyezettek trip_bikes_allowed Egész szám 0 - ismeretlen, 1 - igen, 2 - nem Azt jelzi, hogy megengedett-e a kerékpár szállítás.
Beszálló ajtó trip_boarding_door Egész szám 0 - bármelyik, 1 - első ajtó Azt jelzi, hogy melyik ajtón lehet-e felszállni.
Érkezési idő stop_arrival_time Idő Érkezési idő egy adott megállóhelyen egy adott utazáshoz.
Indulási idő stop_departure_time Idő Indulási idő egy adott megállóból egy adott utazáshoz.
Megálló azonosítója stop_id Szöveg Megállóhelyet, állomást vagy állomás bejáratát azonosítja.
Megálló neve stop_name Szöveg A megálló neve.
Megálló helye (szélesség) stop_latitude Szám -90 - +90 A megálló koordinátájának szélességi foka.
Megálló helye (hosszúság) stop_longitude Szám -180 - +180 A megálló kordinátájának hosszúsági foka.
Megálló típusa stop_location_type Egész szám 0 - megálló, 1 - állomás, 2 - állomás bejárat/kijárat A megálló típusa.
Kerekesszékes beszállás stop_wheelchair_boarding Egész szám 0 - ismeretlen, 1 - igen, 2 - nem Azt jelzi, hogy a megállóból lehetséges-e a kerekesszékes felszállás.
In [53]:
display(df)
agency_name route_name route_type route_desc trip_id trip_direction trip_wheelchair_accessible trip_bikes_allowed trip_boarding_door stop_arrival_time stop_departure_time stop_id stop_name stop_latitude stop_longitude stop_location_type stop_wheelchair_boarding
0 BKK 7G 2 Cinkotai autóbuszgarázs / Újpalota, Nyírpalota út B870931 0 1 2 0 1900-01-01 03:50:00 1900-01-01 03:50:00 008569 Cinkotai autóbuszgarázs 47.498051 19.236675 0 2
1 BKK 7G 2 Cinkotai autóbuszgarázs / Újpalota, Nyírpalota út B870931 0 1 2 0 1900-01-01 03:50:00 1900-01-01 03:50:00 F03291 Injekcióüzem 47.496206 19.231971 0 1
2 BKK 7G 2 Cinkotai autóbuszgarázs / Újpalota, Nyírpalota út B870931 0 1 2 0 1900-01-01 03:51:00 1900-01-01 03:51:00 F03403 EGIS Gyógyszergyár 47.497054 19.224595 0 1
3 BKK 7G 2 Cinkotai autóbuszgarázs / Újpalota, Nyírpalota út B870931 0 1 2 0 1900-01-01 03:52:00 1900-01-01 03:52:00 F03402 Zsemlékes út 47.503095 19.214434 0 1
4 BKK 7G 2 Cinkotai autóbuszgarázs / Újpalota, Nyírpalota út B870931 0 1 2 0 1900-01-01 03:53:00 1900-01-01 03:53:00 F03400 Petőfi tér 47.506473 19.211007 0 1
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
5723094 MÁV-HÉV H5 5 Batthyány tér / Szentendre H8000_22 0 0 0 0 1900-01-02 00:48:00 1900-01-02 00:48:00 F04692 Budakalász, Lenfonó 47.621692 19.046912 0 0
5723095 MÁV-HÉV H5 5 Batthyány tér / Szentendre H8000_22 0 0 0 0 1900-01-02 00:50:00 1900-01-02 00:50:00 F04793 Szentistvántelep 47.629301 19.043159 0 0
5723096 MÁV-HÉV H5 5 Batthyány tér / Szentendre H8000_22 0 0 0 0 1900-01-02 00:52:00 1900-01-02 00:53:00 F04690 Pomáz 47.643188 19.032032 0 0
5723097 MÁV-HÉV H5 5 Batthyány tér / Szentendre H8000_22 0 0 0 0 1900-01-02 00:57:00 1900-01-02 00:57:00 F04688 Pannóniatelep 47.652488 19.065294 0 0
5723098 MÁV-HÉV H5 5 Batthyány tér / Szentendre H8000_22 0 0 0 0 1900-01-02 01:00:00 1900-01-02 01:00:00 009273 Szentendre 47.661038 19.075607 0 0

5723099 rows × 17 columns

Budapest közigazgatási határai¶

Későbbi vizualizációkhoz felhasználjuk Budapest közigazgatási határait is. A letölthető fájl egy KML, de az egyszerűség kedvéért ezt kézzel átalakítottuk CSV formátumra.

In [54]:
## ----- Imports -----

import matplotlib.path as mplPath
from datashader.utils import lnglat_to_meters

## ----- Variables -----

budapest_csv_path = "data/budapest.csv"

## ----- Scrips -----

# Import the border of Budapest
budapest = pd.read_csv(budapest_csv_path)
budapest["x"], budapest["y"] = lnglat_to_meters(budapest.lon, budapest.lat)
budapest_border = mplPath.Path(budapest[["x","y"]])

Google Places adatok beszerzése, és előkészítése¶

A Places API lehetőséget ad arra, hogy a Google maps adatait elérjük, többek között kategória szerinti keresésekkel is. A kategóriák között közvetlen meg lehet adni hogy bárokra szeretnénk keresni. Az eredeti koncepció szerint csak az elérhető megállók környékén történt volna bárokra keresés, de mivel nagyjából 2 óra tömegközlekedéssel a teljes vros bejrható, ezért a teljes városra lekérdeztük a bárokat.

Az ehhez használt API endpoint a nearbysearch volt, aminek segítségével nagyjából egy kör sugarú területen belül a keresési feltételeknek megfelelő POI-kat mind megkaphatjuk. Az a módszer, amivel a keresés történt sérti a Google Maps ToS-ét, ezért ismétlése nem ajánlott, a kódot érdeme átdolgozni a ToS-el összhangban. Ez többek között azt jelenti, hogy csak online lehet az adatokat elérni, offline tárolásuk nem megengedett. Valószínűleg egy lehetséges alternatíva, hogy a grid mentén történő lekérdezés helyett egy körrel egész Budapestet lefedve lehet az összes bárt lekérdezni, amelyek Budapest területén találhatóak.

Az API használatához egy .env fileban, vagy környezeti változóként meg kell adni egy Google API kulcsot, ami fel van készítve Places API használatára.

Az endpointhoz egy python wrappert használtunk, ahol megoldott az Error handling, de a paging nem. Így megkaptunk egy listát az elérhető helyekről, Place formátumban. A Place formátum:

Változó név Típus Tartalom
name string bár neve
business_status string státusz
geometry Geometry leíró geometria a helyhez
place_id string id a Place objetumhoz

és még sok más...

Ezek a legfontosabb informácók. A Geometry típusból kinyerhetőek a koordinátái a bárnak, ami alapján el lehet őket helyezni a térképen. Ezeket az információkta át is transzformáltuk, és az így kapott végleges adatokat elmentettük Parquet formátumban.

In [55]:
## ----- Imports -----
import numpy as np
from src.Places import Places

## ----- Variables -----

places_parquet_path = "temp/bars.parquet.gzip"


## ----- Script -----
def import_bars():
    if not os.path.exists(places_parquet_path):
        raise PermissionError("This is in direct violation with Google place ToS. \n\
Use only if you accept this. The code will be rewritten to be comilant.")
        # Determine the minimum and maximum coordinates
        min_lon, max_lon = min(budapest.lon), max(budapest.lon)
        min_lat, max_lat = min(budapest.lat), max(budapest.lat)

        # Create grid
        resolution = 50
        range_lon = np.linspace(min_lon, max_lon, resolution)
        range_lat = np.linspace(min_lat, max_lat, resolution)
        places_grid = pd.DataFrame({"lat": range_lat}).join(pd.DataFrame({"lon": range_lon}), how='cross')
        places_grid["x"], places_grid["y"] = lnglat_to_meters(places_grid.lon, places_grid.lat)

        # Determine points inside of Budapest
        places_grid = places_grid[budapest_border.contains_points(places_grid[["x", "y"]])]

        if places_grid.size > 8000:
            raise ValueError("May too big request, check if you would like to proceed!!")

        # Use places api interface here to load all points
        client = Places()
        all_bars = client.get_bars_batch(places_grid, radius=425)
        all_bars.drop_duplicates("place_id", inplace=True)

        # Now all bars are available as points
        all_bars["x"], all_bars["y"] = lnglat_to_meters(all_bars.bar_lon, all_bars.bar_lat)
        all_bars.to_parquet(places_parquet_path)

    else:
        # Load from parquet file
        all_bars = pd.read_parquet(places_parquet_path).drop_duplicates("place_id")

    return all_bars


bars = import_bars()

Holoviews inicializálás¶

In [56]:
## ----- Imports -----

import holoviews as hv
from holoviews.element.tiles import OSM
from holoviews.operation.datashader import *
import warnings

## ----- Variables -----

image_height=500
image_width=750
colormap_to_use='RdYlGn_r'

## ----- Script -----

# Initialize holoviews
warnings.filterwarnings("ignore")
hv.extension('bokeh', logo=False)
clipping = {'NaN': '#00000000'}
hv.opts.defaults(
  hv.opts.Image(cmap=colormap_to_use,
                height=image_height, width=image_width,
                colorbar=True,
                tools=['hover'], active_tools=['wheel_zoom'],
                clipping_colors=clipping),
  hv.opts.Tiles(active_tools=['wheel_zoom'], height=image_height, width=image_width),
  hv.opts.Points(active_tools=['wheel_zoom'], height=image_height, width=image_width)
)

map_tiles  = OSM()
map_tiles_05  = OSM().opts(alpha=0.5, bgcolor='black')

# Convert latitude and longitude to meters that holoviews can understand
df["stop_x"], df["stop_y"] = lnglat_to_meters(df.stop_longitude, df.stop_latitude)

EDA¶

Közeli megállók meghatározása¶

Első lépésként a BME I épület 10 perces sétatávolságban található megállókat kell megtalálni. Ehhez első sorban megállíptjuk a BME I épület koordinátáját (47.4724702,19.0597401). Feltételezhetjük, hogy egy ember normál tempóban 4 km/h sebességgel sétál, valamint a megállókat légvonalban keressük, mivel a ténylegesen gyalogosan megközelíthető útvonalak meghatározása lényegesen bonyolítaná a feladatot. A koordináták közötti távolságot a Haversine formula segítségével határozzuk meg.

In [57]:
## ----- Variables -----

bme_i_lon = 19.0597401
bme_i_lat = 47.4724702
max_minutes = 10 # min
max_speed = 4 # km/h
avg_earth_radius = 6371.0088

## ----- Helpers -----

def distance(lat1, lng1, lat2, lng2):
    # convert all latitudes/longitudes from decimal degrees to radians
    lat1 = np.radians(lat1)
    lng1 = np.radians(lng1)
    lat2 = np.radians(lat2)
    lng2 = np.radians(lng2)

    # calculate haversine
    lat = lat2 - lat1
    lng = lng2 - lng1
    d = np.square(np.sin(lat * 0.5)) + np.cos(lat1) * np.cos(lat2) * np.square(np.sin(lng * 0.5))
    return 2 * avg_earth_radius * np.arcsin(np.sqrt(d))

def findNearStops():
    # Determine the near stops from BME building I
    max_distance = max_speed * max_minutes / 60 # km
    df["bme_i_distance"] = distance(bme_i_lat, bme_i_lon, df.stop_latitude, df.stop_longitude)
    return df[df["bme_i_distance"] <= max_distance]

## ----- Script -----

near_stops = findNearStops()
near_stops_points = hv.Points(near_stops.drop_duplicates("stop_id"), kdims=["stop_x","stop_y"], vdims=["stop_name", "bme_i_distance"]).opts(color="r",size=10, tools=['hover'], title="Az I épülettől 10 perces sétatávolságban levő megállók")
map_tiles * near_stops_points
Out[57]:

Éjszakai járatok szűrése¶

Az adathalmaz nem tartalmaz információt arról, hogy egy járat éjszakai-e vagy nem, így feltételezzük, hogy éjszakai járat a BKK meghatározása alapján a 6-os villamos, a 200E busz, illetve a 900-as jelzésű buszok. Tekintettel arra, hogy a 6-os villamos és a 200E busz egész nap közlekedik, tekintsük éjszakai járatnak azokat, melyek 23:00 óra és 04:00 óra között közlekednek, mivel a legtöbb éjszakai busz is hasonló időben jár.

In [58]:
# import bamboolib as bam

# Step: Group by and aggregate
near_routes_list = near_stops.groupby(['route_name','route_type']).agg(route_name_size=('route_name', 'size')).reset_index()

print("Járművek, amelyek megállnak a közeli megállókban: ")
for route in near_routes_list['route_name']:
    print(route + ' ',end='')

print('\nEbből villamos:')
for route in near_routes_list[near_routes_list['route_type'] == 0]['route_name']:
    print(route + ' ',end='')

print('\nEbből busz:')
for route in near_routes_list[near_routes_list['route_type'] == 2]['route_name']:
    print(route + ' ',end='')
Járművek, amelyek megállnak a közeli megállókban: 
1 107 133E 153 154 212 212A 212B 33 4 41 6 901 918 
Ebből villamos:
1 4 41 6 
Ebből busz:
107 133E 153 154 212 212A 212B 33 901 918 
In [59]:
# Collect the routes, that are available from the near stops
available_trips_ids = near_stops.trip_id.unique()
available_trips = df[df.trip_id.isin(available_trips_ids)]

# Filter night routes
time_23 = datetime.strptime("23:00", "%H:%M")
time_04 = datetime.strptime("04:00", "%H:%M")
night_lines = available_trips[ (available_trips.route_name.str.match('^9\d\d$')) |  (available_trips.route_name.str.match('^(6|200E)$'))]

# BKK specified night lines
night_routes = night_lines[(night_lines.stop_departure_time >= time_23 ) | (night_lines.stop_departure_time <= time_04 )]

# Other lines in the night
routes_at_night = available_trips[(available_trips.stop_departure_time >= time_23 ) | (available_trips.stop_departure_time <= time_04 )]
In [60]:
# Fix datetime representation of stops after midnight
routes_at_night['corrected_stop_departure_time'] = routes_at_night['stop_departure_time'].transform(lambda x: x + timedelta(days=1) if x <= time_04 else x)
routes_at_night['corrected_stop_arrival_time'] = routes_at_night['stop_arrival_time'].transform(lambda x: x + timedelta(days=1) if x <= time_04 else x)
night_routes['corrected_stop_departure_time'] = night_routes['stop_departure_time'].transform(lambda x: x + timedelta(days=1) if x <= time_04 else x)
night_routes['corrected_stop_arrival_time'] = night_routes['stop_arrival_time'].transform(lambda x: x + timedelta(days=1) if x <= time_04 else x)

Globálisan is megfigyelhető, hogy a Budapesti tömegközlekedés leginkább 05:30 és 23:59 között aktív. Ez alátámasztja, hogy éjszakainak tekintjük az ebben az időszakban közlekedő járműveket.

In [61]:
import plotly.express as px
fig = px.histogram(available_trips.sample(n=10000, replace=False, random_state=123).sort_index(), x='stop_departure_time',
title= "Budapesti tömegközlekedés gyakorisága",labels= {'stop_departure_time':'Időpont [30 perc]'})
fig.update_layout(yaxis_title="Darabszám")
fig

Az EDA eszköze esetünkben a pandas_profiling lesz, aminek segítségével gyorsan, és egyszerűen tudunk egy DataFrameről felderítő adatelemzést végezni.

In [62]:
# Quick data EDA with pandas_profiling
from pandas_profiling import ProfileReport
profile = ProfileReport(routes_at_night, title="Nigth Routes near I building",html={'full_width':True})
profile
Summarize dataset:   0%|          | 0/5 [00:00<?, ?it/s]
Generate report structure:   0%|          | 0/1 [00:00<?, ?it/s]
Render HTML:   0%|          | 0/1 [00:00<?, ?it/s]
Out[62]:

In [63]:
#Show convergence Matrice in full display
import plotly.graph_objects as go

df_corr = routes_at_night.corr()

fig = go.Figure()
fig.add_trace(
    go.Heatmap(x = df_corr.columns,y = df_corr.index,z = np.array(df_corr),text=df_corr.values,texttemplate='%{text:.2f}',
    colorbar=dict(thickness=5, tickvals=[-1, 1], ticktext=['-1', '1'], outlinewidth=0),
    colorscale='RdBu')
)
fig.layout.width = 800
fig.layout.height = 800
fig.layout.title = "Korrelációs mátrix az adatokról."
fig.show()

EDA profiling eredménye:¶

  • A BKK által csak a 6-os villamos van számontartva mint éjszakai villamos, ugyanakkor a 23:00 és 4:00 közötti időszakban a 4-es villamos is relatív gyakran jár
  • a 23:00 és 5:00 közötti idszakban az I-hez közeli megállókból ~2400 különböző járat indul
  • a közeli megállók párban szerepelnek, így mindkét irányban ugyanolyan frekvenciával járnak a járatok. (Ez nem minden esetben teljesül, léteznek aszimmetrikus járatok, pl 34, 134,934)
  • az éjszaka közlekedő járatok jelentős hányadán nem lehet biciklit szállítani
  • érdekes módon erős lienáris kapcsolat van a szélességi fok, és az éjszakai járatokkal elérhető megállók I-től vett távolsága között
  • megfigyelhető, hogy az összetartozó adatok között erős a korreláció :P
  • megfigyelhető egy erős korreláció a járat neve, és a biciklik utaztatásának legalitása között: biciklit csak az 1-es illetve 41 villamosokon lehet utaztatni az elérhető vonalak közül, illetve a 212-es buszon.

Megfigyelhető, hogy mindössze pár éjszakai járat minősül bicikliszállításra alkalmasnak

In [64]:
# Display bike allowed routes
tmp = routes_at_night[routes_at_night['trip_bikes_allowed'] == 1]["route_name"].unique()
print( "Járatok az I közelében, ahol egyáltalán lehetőség van bicikli szállításra:")
for i in tmp:
    print(i,end=', ')
Járatok az I közelében, ahol egyáltalán lehetőség van bicikli szállításra:
1, 41, 212, 
In [65]:
# Step: Sort column(s) corrected_stop_departure_time ascending (A-Z)
routes_at_night = routes_at_night.sort_values(by=['corrected_stop_departure_time'], ascending=[True])

#supressed bamboolib explorer
routes_at_night
Out[65]:
agency_name route_name route_type route_desc trip_id trip_direction trip_wheelchair_accessible trip_bikes_allowed trip_boarding_door stop_arrival_time ... stop_name stop_latitude stop_longitude stop_location_type stop_wheelchair_boarding stop_x stop_y bme_i_distance corrected_stop_departure_time corrected_stop_arrival_time
3434742 BKK 1 0 Kelenföld vasútállomás M / Bécsi út / Vörösvár... C56318980 1 1 2 1 1900-01-01 23:00:00 ... Erzsébet királyné útja, aluljáró 47.516400 19.091909 0 1 2.125302e+06 6.026775e+06 5.449977 1900-01-01 23:00:00 1900-01-01 23:00:00
3881290 BKK 212 2 Boráros tér H / Normafa, látogatóközpont C56575834 1 1 1 1 1900-01-01 23:00:00 ... Óra út 47.504525 18.998004 0 1 2.114848e+06 6.024818e+06 5.850018 1900-01-01 23:00:00 1900-01-01 23:00:00
1757746 BKK 1 0 Kelenföld vasútállomás M / Bécsi út / Vörösvár... C53761157 0 1 2 1 1900-01-01 23:00:00 ... Hengermalom út / Szerémi út 47.462551 19.048509 0 1 2.120470e+06 6.017904e+06 1.388976 1900-01-01 23:00:00 1900-01-01 23:00:00
3881250 BKK 212 2 Boráros tér H / Normafa, látogatóközpont C56575830 1 1 1 1 1900-01-01 23:00:00 ... Petőfi híd, budai hídfő 47.476573 19.059268 0 1 2.121668e+06 6.020213e+06 0.457589 1900-01-01 23:00:00 1900-01-01 23:00:00
3881206 BKK 212 2 Boráros tér H / Normafa, látogatóközpont C56575827 0 1 1 1 1900-01-01 23:00:00 ... Sirály utca 47.490555 19.018489 0 2 2.117129e+06 6.022516e+06 3.695090 1900-01-01 23:00:00 1900-01-01 23:00:00
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
1717002 BKK 1 0 Kelenföld vasútállomás M / Bécsi út / Vörösvár... C537456119J 1 2 1 1 1900-01-01 04:00:00 ... Hős utca 47.495706 19.109078 0 1 2.127213e+06 6.023365e+06 4.518971 1900-01-02 04:00:00 1900-01-02 04:00:00
2832953 BKK 918 2 Kelenföld vasútállomás M / Óbudai autóbuszgarázs C55577866 1 1 2 1 1900-01-01 04:00:00 ... Kacsóh Pongrác út 47.517806 19.089562 0 2 2.125040e+06 6.027007e+06 5.516584 1900-01-02 04:00:00 1900-01-02 04:00:00
3470049 BKK 6 0 Széll Kálmán tér M / Móricz Zsigmond körtér M C5634241654 1 1 2 0 1900-01-01 04:00:00 ... Mester utca / Ferenc körút 47.482751 19.068848 0 1 2.122734e+06 6.021230e+06 1.332436 1900-01-02 04:00:00 1900-01-02 04:00:00
3872160 BKK 901 2 Kelenföld vasútállomás M / Bécsi út / Vörösvár... C56568102 0 1 2 0 1900-01-01 04:00:00 ... Népliget M 47.476330 19.099513 0 2 2.126148e+06 6.020173e+06 3.019942 1900-01-02 04:00:00 1900-01-02 04:00:00
3872729 BKK 901 2 Kelenföld vasútállomás M / Bécsi út / Vörösvár... C5656864 1 1 2 0 1900-01-01 04:00:00 ... Albert Flórián út 47.472525 19.095428 0 1 2.125693e+06 6.019546e+06 2.682368 1900-01-02 04:00:00 1900-01-02 04:00:00

37489 rows × 22 columns

Járatok éjszaka az I környékéről indulva¶

Ha megvizsgáljuk az I környékéről induló járatokat, akkor megfigyelhető egy egyértelmű vágás, ami nagyjából éjfél környékén kezdődik. Ezután már az éjszakai járatok dominálnak, amiknek a járatsűrűsége lényegesen kisebb. Hisztogramon ábrázolva egyértelműen látszik, hogy a 133E, 4-es villamos, 1-es villamos, 212-es busz csak éjfélig járnak, ezután már leginkább csak dedikált éjszakai járatok: a 6, 901 és 918 járatok járnak.Az is látszik, hogy a 901-es buszok megközelítőleg kerek időpontokban, félóránként járnak az I környéki megállókban.

In [66]:
import plotly.express as px
dat = routes_at_night.sample(n=10000, replace=False, random_state=123).sort_index()
fig = px.histogram(dat[dat['stop_id'].isin(near_stops['stop_id'])], x='corrected_stop_departure_time',color='route_name',
                   color_discrete_sequence = px.colors.qualitative.T10 ,barmode='relative',title="Éjszaka 23:00 és 4:00 közötti járatok amelyek az I épület környékén indulnak.",
                   labels={'corrected_stop_departure_time':'Indulás időpontja [25 perc]', 'route_name':'Járat'})
fig.update_layout(yaxis_title="Darabszám")
fig

Overlay hisztogram¶

Jól megfigyelhető a periódikusság az I közeli megállókon, amit a végighaladó buszok alakítanak ki.

In [67]:
import plotly.express as px
dat = night_routes.sample(n=10000, replace=False, random_state=123).sort_index()
fig = px.histogram(dat[dat['stop_id'].isin(near_stops['stop_id'])], x='corrected_stop_arrival_time',
color='route_name',barmode='overlay',title="I környéki éjszakai járatok sűrűségfüggvényei",
labels={'count':'db','corrected_stop_arrival_time':'Indulás időpontja [25 perc]', 'route_name': 'Járat'})
fig.update_layout(yaxis_title="Darabszám")
fig

Kategórikus adatok¶

Az éjszakai járatok kategórikus adatainak kissé unortodox módja lehet egy folyam diagram: látható, hogy a járatok jelentős része a 6-os villamos, ami az egyetlen elérhető villamos. Jellemzően első ajtós felszállással múködik az összes éjszakai busz, bár az adat pontossága megkérdőjelezhető, hiszan első ajtós felszállás nem jellemző a villamosokon.

In [68]:
import plotly.express as px
paralell_data = night_routes.sample(n=10000, replace=False, random_state=123).sort_index()
paralell_data["color"] = paralell_data.route_name.map({"6":"#ffd800", "901":"#000000", "918":"#75597B"})
paralell_data["route_type"] = paralell_data.route_type.map({0:"villamos", 2:"busz"})
paralell_data["trip_direction"] = paralell_data.trip_direction.map({0:"normál", 1:"ellentétes"})
paralell_data["trip_boarding_door"] = paralell_data.trip_boarding_door.map({0:"bármelyik", 1:"első ajtó"})
fig = px.parallel_categories(paralell_data, dimensions=['route_name', 'route_type', 'trip_direction', 'trip_boarding_door'],
labels={'route_name':'Útvonal neve', 'route_type':'Jármű típusa', 'trip_direction':'Járat iránya', 'trip_boarding_door':'Megengedett felszállási ajtó'}
, color="color",title="Éjszakai járatok főbb tulajdonságai")
fig

Big data vizualizáció¶

A vizualizációhoz első lépésben létrehoztunk egy grid-et, ami Budapest közigazgatási határain belüli pontokat tartalmazza. A szükséges utazái idők meghatározását az egyes grid pontokra 2 lépésben határoztuk meg. Első lépésben vizsgáltuk a triviális esetet, hogy mennyi idő elsétálni (a korábbiakhoz hasonlóan légvonalban) az adott pontokhoz az I épülettől. A következő lépésben ugyanezekre a pontokra meghatároztuk azt is, hogy mennyi idő eljutni tömegközlekedéssel (több opció esetén vettük a minimumot) és végül a két idő közül vettük a kisebbet. A triviális esetet azért volt szükséges vizsgálni, mert az egyetemhez közeli területeken számos olyan pont van, ahova gyorsabb elsétálni, mint tömegközlekedéssel elmenni.

A két idő meghatározása közül a tömegközlekedéses verzió, ami összetettebb. Ez is több lépésben valósult meg. Első lépésben meghatároztuk, hogy az egyes megállókhoz mennyi idő elsétálni, majd megkerestük azokat a járatokat, melyek ez az időpont (este 10 + sétálással eltelt idő) után érkeznek a megállóba. Összesítettük, hogy ezek a járatok segítségével melyik másik megállókba lehet eljutni, és mikorra érkezünk meg velük, majd szűrtük, hogy minden megállóhoz csak a legkorábbi érkezési időt tartottuk meg. Ezzel megkaptuk, hogy este 10-es indulással mikorra lehet eljutni a város különböző pontjaira. Ezután az utolsó feladat az volt, hogy meghatározzuk, hogy ideális esetben az egyes grid pontokhoz mennyi idő eljutni. Ehhez azt kellet, hogy kiszámoljuk, hogy az egyes grid ponthoz, mennyi idő eljutni az egyes megállókból, kiegészítve az oda utazási időkkel és ezek közül választottuk a legkisebbet.

Végül az eredményeket vizualizáltuk egy heatmapen.

In [69]:
import numpy as np
from math import ceil

def visualization():
    # Determine the minimum and maximum coordinates
    min_lon, max_lon = min(budapest.lon), max(budapest.lon)
    min_lat, max_lat = min(budapest.lat), max(budapest.lat)

    # Create grid
    resolution = 150
    range_lon = np.linspace(min_lon, max_lon, resolution)
    range_lat = np.linspace(min_lat, max_lat, resolution)
    heatmap = pd.DataFrame({"lat": range_lat}).join(pd.DataFrame({"lon": range_lon}), how='cross')
    heatmap["x"], heatmap["y"] = lnglat_to_meters(heatmap.lon, heatmap.lat)

    # Determine points inside of Budapest
    heatmap = heatmap[budapest_border.contains_points(heatmap[["x", "y"]])]

    # Calculate walk time
    heatmap["walk_min"] = distance(bme_i_lat, bme_i_lon, heatmap.lat, heatmap.lon) / max_speed * 60

    # Determine when you can get to each stops on walk
    t = datetime.strptime("22:00", "%H:%M")
    df["bme_i_walk"] = df.bme_i_distance.apply(lambda d: t + timedelta(hours=d / max_speed))

    # Filter the available trips based on the departure time
    walk = df[df.stop_departure_time >= df.bme_i_walk][["trip_id", "stop_id", "stop_departure_time"]]

    # Find the available arrival stops
    stops = walk.merge(df[["trip_id","stop_id", "stop_arrival_time", "stop_longitude", "stop_latitude", "stop_x", "stop_y"]], left_on="trip_id", right_on="trip_id", suffixes=["_from", "_to"])
    stops = stops[(stops.stop_id_from != stops.stop_id_to) & (stops.stop_arrival_time > stops.stop_departure_time)]

    # Find the earliest time you can get each stop and calculate the required time
    stops = stops.groupby(['stop_id_to'], as_index=False).apply(lambda x: x.iloc[x.stop_departure_time.argmin(),])
    stops["bme_i_min"] = stops.stop_arrival_time.apply(lambda at: (at-t).total_seconds() / 60)
    stops = stops.reset_index()[["stop_longitude", "stop_latitude", "stop_x", "stop_y", "bme_i_min"]].drop_duplicates()

    # Find the stop for every grid point from where you can get to there at earliest and calculate that time
    heatmap = heatmap.join(stops, how="cross")
    heatmap["bkk_min"] = distance(heatmap.lat, heatmap.lon,heatmap.stop_latitude, heatmap.stop_longitude)/max_speed*60 + heatmap.bme_i_min
    heatmap = heatmap.groupby(["lat", "lon"], as_index=False).apply(lambda x: x.iloc[x.bkk_min.argmin(),])

    # Determine the minimum time (choose between walk or travel + walk)
    heatmap["total_min"] = heatmap[["bkk_min","walk_min"]].min(axis=1)

    # Determine sampling for holoview
    min_x, min_y = lnglat_to_meters(min_lon,min_lat)
    max_x, max_y = lnglat_to_meters(max_lon,max_lat)
    x_sampling = ceil((max_x-min_x)/resolution)
    y_sampling = ceil((max_y-min_y)/resolution)


    # Display heatmap
    hv_heatmap = hv.HeatMap(heatmap, kdims=["x", "y"], vdims=["total_min"])
    stream = [hv.streams.RangeXY(source=hv_heatmap)]
    hv_heatmap_agg = regrid(aggregate(hv_heatmap, aggregator=ds.min('total_min'), x_sampling=x_sampling, y_sampling=y_sampling, streams=stream), upsample=True, interpolation='linear').opts(cmap='RdYlGn_r', alpha=0.5, colorbar=True, clabel='Utazási idő (perc)', clipping_colors={'NaN':'transparent'})
    return map_tiles_05*hv_heatmap_agg

hv_heatmap = visualization()
hv_heatmap.opts(hv.opts.Overlay(title='I épülettől számított utazási távolság'))
hv_heatmap
Out[69]:

A heatmapen jól kivehetőek a főbb tömegközlekedési vonalak. Például sötét zöld szín mutatja a 4-6-os illetve az 1-es villamos vonalát, mivel ezek mind gyakran járnak és közel vannak az I épülethez. Ezen felül még meghatározó a 33-as és 133E jelzésű busz vonala, illetve az egyéb, pl 17-es villamos vonala. Ami még jól látszik a térképen, hogy Budapest tömegközlekedési hálózata nyilvánvalóan nem egyenletes. Például egész jól kivehető, hogy a reptérre viszonylag gyorsan, kb 60 perc alatt ki lehet jutni az egyetemtől, viszont ha egy kicsit arrébb mennénk, pl Rákoshegyre, akkor ennek duplájára van szükség, annak ellenére, hogy távolságban közelebb van.

A kapott heatmapet tovább vizsgálhatjuk, ha kombináljuk az elérhető kocsmákkal:

In [70]:
hv_places = hv.Points(bars, kdims=["x", "y"], vdims=["name", "vicinity", "rating"],label='kocsmák').opts(color="blue",size=8, tools=['hover'])
bar_plot = hv_heatmap*hv_places
bar_plot.opts(hv.opts.Overlay(title='I épülettől számított utazási távolság percben + kocsmák'))
bar_plot
Out[70]:

A kocsmák tekintetében látható, hogy ezek leginkább a körúton belül helyezkednek el, és ezen kívül lényegesen kevesebb található. Továbbá érdekes asszimetriát mutat, hogy Pesten sokkal több található, mint Budán, bár ez feltehetően a Gellért hegy miatt is lehet. A leggyorsabban elérhetőek az egyetem közvetlen közelében találhatóak, például a Budafoki, Bartók Béla és Karinthy Frigyes úton, de egész sokat lehet találni még a Duna túloldalán, a Boráros tér közelében is.

Elemzés¶

Az elérhető megállók megtalálását k darab átszállással hasonló képpen tudjuk megcsinálni, mint a korábbi feladatban. Feltételezzük, hogy maximum 10 percet sétálhatunk egyszerre. Így első lépésben vesszük azokat a megállókat, amik elérhetőek 10 perc sétán belül az I épülethez, majd megkeressük azokat a megállókat, ahova el lehet jutni tömegközlekedéssel ezekből a megállókból, majd végül még vizsgáljuk azokat a megállókat, amelyekbe el tudunk jutni ezekből 10 percen belül. Mindegyik esetben végig vizsgáljuk, hogy ne fussunk ki az előírt időből. A k>1 esetben ugyanezt iteráljuk tovább, tehát a korábbi esetben megkapott megállókhoz vesszük azokat, amelyekhez el tudunk jutni tömegközlekedéssel, valamint vesszük azokat, amelyek gyalog megközelíthetőek innen. A vizualizáción a k = 0, 1 és 2 eseteket láthatjuk.

In [71]:
def findStopsK(k):
    t_22_00 = datetime.strptime("22:00", "%H:%M")
    t_22_45 = datetime.strptime("22:45", "%H:%M")
    min10 = 10
    t_min10 = timedelta(minutes=min10)

    # Find the available stops on foot
    stops_available = df[df.bme_i_walk-t_22_00<=t_min10][["stop_id", "bme_i_walk"]].drop_duplicates()
    stops_available = stops_available.rename(columns={'bme_i_walk': 'end_time'})
    all_available = stops_available.stop_id.unique()

    # Find available options within timeframe
    df_in_time = df[["trip_id", "stop_id", "stop_longitude", "stop_latitude", "stop_x", "stop_y", "stop_departure_time", "stop_arrival_time"]][(df.stop_departure_time >= t_22_00) & (df.stop_arrival_time <= t_22_45)]

    # Find travel options between stops
    options = df_in_time[['trip_id','stop_id','stop_departure_time']]\
        .set_index('trip_id')\
        .join( df_in_time[['trip_id', 'stop_id', 'stop_arrival_time']].set_index('trip_id'), lsuffix='_from', rsuffix='_to')\
        .reset_index()
    options = options[options.stop_departure_time < options.stop_arrival_time][['trip_id', 'stop_id_from', 'stop_departure_time', 'stop_arrival_time','stop_id_to']]

    # Find walk connections between stops
    connections = df[["stop_id", "stop_longitude", "stop_latitude"]].drop_duplicates()
    connections = connections.join(connections, how="cross", lsuffix="_from", rsuffix="_to")
    # connections = connections[connections.stop_id_from == connections.stop_id_to] # if we do not want to count the options when walk from one stop to other
    connections["walk_min"] = distance(connections.stop_latitude_from, connections.stop_longitude_from, connections.stop_latitude_to, connections.stop_longitude_to) / max_speed * 60
    connections = connections[connections.walk_min <= min10][["stop_id_from","stop_id_to", "walk_min"]]
    connections["walk_min"] = connections.walk_min.apply(lambda t: timedelta(minutes=t))

    for _ in range(k+1):
        # Travel from one stop to other
        stops_available = stops_available.merge(options, left_on="stop_id", right_on="stop_id_from")
        stops_available = stops_available[stops_available.end_time < stops_available.stop_departure_time]
        stops_available = stops_available[["stop_id_to", "stop_arrival_time"]].drop_duplicates()
        stops_available = stops_available.rename(columns={'stop_id_to': 'stop_id'})
        all_available = np.unique(np.append(all_available,stops_available.stop_id.unique()))

        # Walk from the stop
        stops_available = stops_available.merge(connections, left_on="stop_id", right_on="stop_id_from")
        stops_available["end_time"] = stops_available.stop_arrival_time + stops_available.walk_min
        stops_available = stops_available[stops_available.end_time <= t_22_45][["stop_id_to", "end_time"]].drop_duplicates()
        stops_available = stops_available.rename(columns={'stop_id_to': 'stop_id'})
        all_available = np.unique(np.append(all_available,stops_available.stop_id.unique()))

    stops = pd.DataFrame({"stop_id": all_available}).set_index("stop_id").join(df[["stop_id", "stop_x", "stop_y", "stop_name"]].set_index("stop_id").drop_duplicates()).reset_index()
    
    return stops


# Filter lists to visualize points individually
hv_stops_k0 = hv.Points(findStopsK(0), kdims=["stop_x","stop_y"], vdims=["stop_name"], label="k=0").opts(color='g',size=10, tools=['hover'])
hv_stops_k1 = hv.Points(findStopsK(1), kdims=["stop_x","stop_y"], vdims=["stop_name"], label="k=1").opts(color='y',size=9, tools=['hover'])
hv_stops_k2 = hv.Points(findStopsK(2), kdims=["stop_x","stop_y"], vdims=["stop_name"], label="k=2").opts(color='r',size=8, tools=['hover'])
res = map_tiles * hv_stops_k2 * hv_stops_k1 * hv_stops_k0
res.opts(hv.opts.Overlay(title='I épülettől elérhető megállók k átszállással'))
res
Out[71]:

Az ábrán látható, hogy az átszállás nélküli és 1 átszállással megközelíthető megállók száma jelentős különbséget mutat, viszont ehhez képest a 2 átszállásos megállók már nincsenek jelentősen többen. Ennek feltehető oka, hogy a rendelkezésre álló 45 perces időkeret itt már sokkal jobban korlátozza az utazást. Az átszállás nélküli utazásokat jelentősen a 4-6-os villamos és az 1-es villamos határozza meg, a több átszálássnál már érződik a buszok és metrók hatása is. 2 átszállással Budapest belvárosának nagy részét meg lehet közelíteni, de a külvárosi régiók eléréséhez már nem elég a rendelkezésre álló időkeret.